#include <boost/asio.hpp>
#include <boost/program_options.hpp>

#include <iostream>

#include "Server.h"
#include "Client.h"

namespace po = boost::program_options;
namespace asio = boost::asio;

using namespace std;

struct options {
	string udp_listen_addr;
	string udp_listen_port;
	string tcp_forward_addr;
	string tcp_forward_port;
	string tcp_listen_addr;
	string tcp_listen_port;
	string udp_server_addr;
	string udp_server_port;
	int verbosity;
};

asio::ip::tcp::endpoint udp_to_tcp(const asio::ip::udp::endpoint& udp_ep)
{
	return asio::ip::tcp::endpoint(udp_ep.address(), udp_ep.port());
}

#ifndef WIN32 // Linux?
	#include <net/if.h>
#endif

bool defaultoipv6 = false;

boost::asio::ip::address my_addr_from_string(const std::string& str) {
	if (str.empty()) {
		return boost::asio::ip::address();
	} else if (str == "*") {
		if (!defaultoipv6)
			return boost::asio::ip::address_v4::any();
		else
			return boost::asio::ip::address_v6::any();
	} else if (str == "255.255.255.255")
		return boost::asio::ip::address_v4::broadcast();
#ifndef WIN32 // Linux?
	else if(str.find_first_of("%") != std::string::npos) {
		std::string str_ = str.substr(0, str.find_first_of("%"));
		boost::asio::ip::address_v6 addr = boost::asio::ip::address_v6::from_string(str_);

		std::string ifname = str.substr(str.find_first_of("%")+ 1);

		//in6_addr_type* ipv6_address = static_cast<in6_addr_type*>(addr);
		//bool is_link_local = IN6_IS_ADDR_LINKLOCAL(ipv6_address);
		unsigned long scope_id = 0;
		//if (is_link_local)
			scope_id = if_nametoindex(ifname.c_str());
		if (scope_id == 0)
			scope_id = atoi(ifname.c_str());

		addr.scope_id(scope_id);

		return addr;
	}
#endif
	else
		return boost::asio::ip::address::from_string(str);
}

template<typename EP>
void parse_endpoint(EP& res, const string& ep_addr, const string& ep_port, const string& def_addr, const string& def_port)
{
	std::string addr;
	std::string port;
	if (ep_addr[0] == '[') {
		size_t cbpos = ep_addr.find_last_of("]");
		if (cbpos != std::string::npos) {
			addr = ep_addr.substr(1, cbpos - 1);
			port = ep_addr.substr(cbpos + 1);
		} else
			throw runtime_error("Parse error: no closing ']'");
	} else {
		size_t colpos = ep_addr.find_last_of(":");
		if (colpos != std::string::npos && colpos == ep_addr.find_first_of(":")) {
			addr = ep_addr.substr(0, colpos);
			port = ep_addr.substr(colpos);
		} else
			addr = ep_addr;
	}

	if (addr.empty())
		addr = def_addr;
	if (addr.empty())
		throw runtime_error("Parse error: No address specified");

	if (!ep_port.empty() && !port.empty())
		throw runtime_error("Parse error: Multiple ports specified");

	if (!port.empty()) {
		if (port[0] != ':') throw runtime_error("Parse error: Invalid address");
		port.erase(0, 1);
	}

	if (port.empty())
		port = ep_port;
	if (port.empty())
		port = def_port;
	if (port.empty())
		throw runtime_error("Parse error: No port specified");

	int portnum = atoi(port.c_str()); // todo: better parsing?
	if (portnum < 0 || portnum > 0xffff)
		throw runtime_error(string() + "Parse error: Invalid port: " + port);

	try {
		if (portnum == 0) throw runtime_error(""); // probably text, try to resolve it for real
		asio::ip::address ipaddr = my_addr_from_string(addr);
		res = EP(my_addr_from_string(addr), portnum);
	} catch (std::exception& /* e */) {
		// try as host name
		typedef typename EP::protocol_type proto;
		asio::io_service temp_service;
		typename proto::resolver resolver(temp_service);
		typename proto::resolver::query query(addr, port);
		typename proto::resolver::iterator endpoint_iterator = resolver.resolve(query);

		res = *endpoint_iterator;
	}
}

bool parse_options(options& opt, int argc, char **argv)
{
	try {
		// Declare the supported options.
		po::options_description desc("Allowed options");
		desc.add_options()
			("help", "produce help message")
			("verbose,v", "Increase verbosity")
			("quiet,q", "No output")

			("udp-listen-addr", po::value<string>(&opt.udp_listen_addr), "UDP address to listen on as server")
			("udp-listen-port", po::value<string>(&opt.udp_listen_port), "UDP port to listen on as server")
			("tcp-forward-addr", po::value<string>(&opt.tcp_forward_addr), "TCP address to forward to as server")
			("tcp-forward-port", po::value<string>(&opt.tcp_forward_port), "TCP port to forward to as server")

			("tcp-listen-addr", po::value<string>(&opt.tcp_listen_addr), "TCP address to listen on as client")
			("tcp-listen-port", po::value<string>(&opt.tcp_listen_port), "TCP port to listen on as client")
			("udp-server-addr", po::value<string>(&opt.udp_server_addr), "UDP address to connect to as client")
			("udp-server-port", po::value<string>(&opt.udp_server_port), "UDP port to connect to as client")

			("ipv6,6", po::value<bool>(&defaultoipv6)->default_value(false), "Use ipv6 by default")
		;

		po::variables_map vm;
		po::store(po::parse_command_line(argc, argv, desc), vm);
		po::notify(vm);

		if (vm.count("help")) {
			cout << desc << endl;
			return false;
		}

		opt.verbosity = 1 + vm.count("verbose");
		if (vm.count("quiet")) opt.verbosity = 0;

	} catch (exception& e) {
		cout << "Error parsing command line: " << e.what() << endl;
		cout << "Use --help option for usage" << endl;
		return false;
	}
	return true;
}

int main( int argc, char **argv )
{
	options opt;
	if (!parse_options(opt, argc, argv))
		return 1;

	bool be_server = !opt.udp_listen_addr.empty() || !opt.udp_listen_port.empty() || !opt.tcp_forward_addr.empty() || !opt.tcp_forward_port.empty();
	bool be_client = !opt.tcp_listen_addr.empty() || !opt.tcp_listen_port.empty() || !opt.udp_server_addr.empty()  || !opt.udp_server_port.empty();

#define FAIL(str) do { cerr << "Error: " << str << endl;; return 1; } while (false)

	if (be_server && be_client)
		FAIL("Can not be both server and client.");

	if (!be_server && !be_client)
		FAIL("Need to be either server or client.");

	server_options opt_server;
	client_options opt_client;

	if (be_server) {
		opt_server.verbosity = opt.verbosity;

		try {
			parse_endpoint(opt_server.udp_listen_addr, opt.udp_listen_addr, opt.udp_listen_port, "*", "");
		} catch (std::exception& e) { FAIL("Invalid udp listen address: " << e.what()); };
		try {
			parse_endpoint(opt_server.tcp_forward_addr, opt.tcp_forward_addr, opt.tcp_forward_port, "", "");
		} catch (std::exception& e) { FAIL("Invalid tcp forward address: " << e.what()); };

		if (opt.verbosity >= 1) {
			cout << "Starting server" << endl;
			cout << "udp listen address: " << opt_server.udp_listen_addr << endl;
			cout << "tcp forward address: " << opt_server.tcp_forward_addr << endl;
		}
	}

	if (be_client) {
		opt_client.verbosity = opt.verbosity;

		try {
			parse_endpoint(opt_client.tcp_listen_addr, opt.tcp_listen_addr, opt.tcp_listen_port, "*", "");
		} catch (std::exception& e) { FAIL("Invalid tcp listen address: " << e.what()); };
		try {
			parse_endpoint(opt_client.udp_server_addr, opt.udp_server_addr, opt.udp_server_port, "", "");
		} catch (std::exception& e) { FAIL("Invalid udp server address: " << e.what()); };

		if (opt.verbosity >= 1) {
			cout << "Starting client" << endl;
			cout << "tcp listen address: " << opt_client.tcp_listen_addr << endl;
			cout << "udp server address: " << opt_client.udp_server_addr << endl;
		}
	}

	boost::asio::io_service service;
	boost::scoped_ptr<Server> server;
	boost::scoped_ptr<Client> client;

	try {
		if (be_server) server.reset(new Server(service, opt_server));

		if (be_client) client.reset(new Client(service, opt_client));

		service.run();
	} catch (exception& e) {
		FAIL(e.what());
	}

	return 0;
}
